package dk.itu.KrakMap;

import graphlib.*;
import java.io.IOException;
import java.util.*;

public class KrakMap implements Map {

    private final Graph<KrakEdge, KrakNode> graph;
    private final int separationLevels = 3;
    private final HashMap<ZoomFilter, KrakQuadTree> quads = new HashMap<ZoomFilter, KrakQuadTree>();
    private Viewport maxViewport;
    private LinkedList<Route> bufferedRoutes = new LinkedList<Route>();
    private HashSet<TravelType> lastTravellerProfile;

    /**
     * Constructor for KrakMap.
     * @param dir Specified directory to load from.
     * @throws IOException Fails if there is no files in dir.
     */
    KrakMap(String dir) throws IOException {
        graph = KrakLoader.graphFromFiles(dir + "kdv_node_unload.txt", dir + "kdv_unload.txt");

        Graph<KrakEdge, KrakNode> simpleGraph;

        //Initiate QuadTrees
        for (ZoomFilter f : ZoomFilter.values()) {
            if (f.getSimplifyLevel() == 0) {
                simpleGraph = graph;
            } else {
                simpleGraph = graph;
            }

            KrakQuadTree quad = new KrakQuadTree(getMaxViewport(), 60);
            quads.put(f, quad);
            for (KrakEdge e : simpleGraph.getEdges()) {
                if (e != null && !f.isExcluded(e.getType())) {
                    quad.insert(e);
                }
            }
            simpleGraph = null;
        }
    }

    /**
     * Method to get nearest edges.
     *
     * @param point MapPoint to find nearest edges from.
     * @param minReturn ???
     * @param filter ZoomFilter to get edges from.
     * @return The nearest edges.
     */
    public Iterable<KrakEdge> getNearestEdges(MapPoint point, int minReturn, ZoomFilter filter) {
        HashSet<KrakEdge> res = new HashSet<KrakEdge>();
        for (int i = 5; res.size() < minReturn; i += 5) {
            MapPoint tempPoint1 = point.clone();
            tempPoint1.move(-i, -i);
            MapPoint tempPoint2 = point.clone();
            tempPoint2.move(i, i);
            res = getRoads(new Viewport(tempPoint1, tempPoint2), filter);
        }
        return res;
    }

    /**
     * Method to get nearest edge.
     * @param point MapPoint to find nearest edges from.
     * @param filter ZoomFilter to get edges from.
     * @return KrakEdge nearest to specified point.
     */
    public KrakEdge getNearestEdge(MapPoint point, ZoomFilter filter) {
        KrakEdge near = null;
        double near_dif = Double.POSITIVE_INFINITY;
        for (KrakEdge e : getNearestEdges(point, 15, filter)) {
            double dif = e.v1.getMapPoint().dist(point) + e.v2.getMapPoint().dist(point) - e.getLength();
            if (dif < near_dif) {
                near = e;
                near_dif = dif;
            }
        }
        return near;
    }

    /**
     * Method to get name of nearest edge.
     * Uses the filter with most edges if nothing is specified.
     *
     * @param point MapPoint to find nearest edge from.
     * @return Name of the edge, closest to the specified point.
     */
    public KrakEdge getNearestEdge(MapPoint point) {
        return getNearestEdge(point, ZoomFilter.getMostOpenFilter());
    }

    /**
     * Method to get name of nearest edge.
     * @param point MapPoint to find nearest edge from.
     * @param filter ZoomFilter to get edges from.
     * @return Name of the edge, closest to the specified point.
     */
    public String getNearestEdgeName(MapPoint point, ZoomFilter filter) {
        KrakEdge near = null;
        double near_dif = Double.POSITIVE_INFINITY;
        for (KrakEdge e : getNearestEdges(point, 15, filter)) {
            if (e.getName().equals("")) {
                continue;
            }
            double dif = e.v1.getMapPoint().dist(point) + e.v2.getMapPoint().dist(point) - e.getLength();
            if (dif < near_dif) {
                near = e;
                near_dif = dif;
            }
        }
        return near == null ? "" : near.getName();
    }

    /**
     * Method to get name of nearest edge.
     * Uses the ZoomFilter containing most roads.
     * @param point MapPoint to find nearest edge from.
     * @return Name of the edge, closest to the specified point.
     */
    public String getNearestEdgeName(MapPoint point) {
        return getNearestEdgeName(point, ZoomFilter.getMostOpenFilter());
    }

    /**
     * Getting roads from specified area within the specified filter.
     * @param viewport Actual viewport to get roads from.
     * @param filter ZoomFilter to sort out the roads.
     * @return Returns a HashSet containing the KrakEdges in the specified area.
     */
    public HashSet<KrakEdge> getRoads(Viewport viewport, ZoomFilter filter) {
        return quads.get(filter).query2D(viewport);
    }

    /**
     * Getting roads from specified area within the specified filter.
     * @param downLeft Down-left point of viewport to get roads from.
     * @param upRight Up-right point of viewport to get roads from.
     * @param filter ZoomFilter to sort out the roads.
     * @return Returns a HashSet containing the KrakEdges in the specified area.
     */
    public HashSet<KrakEdge> getRoads(MapPoint downLeft, MapPoint upRight, ZoomFilter filter) {
        return getRoads(new Viewport(downLeft, upRight), filter);
    }

    /**
     * Getting roads from specified area within the specified filter.
     * Uses the ZoomFilter containing most roads.
     * @param viewport Actual viewport to get roads from.
     * @return Returns a HashSet containing the KrakEdges in the specified area.
     */
    public HashSet<KrakEdge> getRoads(Viewport viewport) {
        return getRoads(viewport, ZoomFilter.getMostOpenFilter());
    }

    /**
     * Getting roads from specified area within the specified filter.
     * Uses the ZoomFilter containing most roads.
     * @param downLeft Down-left point of viewport to get roads from.
     * @param upRight Up-right point of viewport to get roads from.
     * @return Returns a HashSet containing the KrakEdges in the specified area.
     */
    public HashSet<KrakEdge> getRoads(MapPoint downLeft, MapPoint upRight) {
        return getRoads(downLeft, upRight, ZoomFilter.getMostOpenFilter());
    }

    /**
     * @return Maximum Viewport of the KrakMap.
     */
    public Viewport getMaxViewport() {
        if (maxViewport != null) {
            return maxViewport;
        }
        double min_x = 1000000000;
        double min_y = 1000000000;
        double max_x = 0;
        double max_y = 0;
        for (KrakNode n : graph.nodes) {
            if (n != null) {
                if (min_x > n.getX()) {
                    min_x = n.getX();
                }
                if (min_y > n.getY()) {
                    min_y = n.getY();
                }
                if (max_x < n.getX()) {
                    max_x = n.getX();
                }
                if (max_y < n.getY()) {
                    max_y = n.getY();
                }
            }
        }
        maxViewport = new Viewport(
                new MapPoint(min_x, min_y),
                new MapPoint(max_x, max_y));
        return maxViewport;
    }

    /**
     * Calls the method getOrderedRoute to return a RoutePlan.
     * @param request RoutePlanRequest to be used for getting a route.
     * @return Returns a RoutePlan from the request.
     */
    public RoutePlan getRoute(RoutePlanRequest request) {
        //Set Weigth ud fra TravelerProfile
        if (lastTravellerProfile == null || !lastTravellerProfile.equals(request.getTravellerProfile())) {
            for (ArrayList<KrakEdge> el : graph.edges) {
                for (KrakEdge e : el) {
                    e.updateWeight(request.getTravellerProfile());
                }
            }
        }
        lastTravellerProfile = request.getTravellerProfile();
        if (request.isStartLocked() && request.isEndLocked() && request.isOrdered()) {
            return getOrderedRoute(request);
        } else {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    }

    /**
     * Creates an ordered route from a RoutePlanRequest.
     * @param request RoutePlanRequest to be used for getting a route.
     * @return Returns a RoutePlan from the request.
     */
    private RoutePlan getOrderedRoute(final RoutePlanRequest request) {
        ArrayList<Route> result = new ArrayList<Route>();
        LinkedList<KrakNode> places = request.getPlaces();
        KrakNode last;
        KrakNode next = places.get(0);


        //Check that start and endpoints are accesible
        for (int i = 0; i < places.size(); i++) {
            KrakNode place = places.get(i);
            last = next;
            next = place;
            boolean lastOutGood = false;
            Iterator<KrakEdge> lastOuts = graph.outGoingEdges(last);
            while (lastOuts.hasNext() && !lastOutGood) {
                if (request.canTravel(lastOuts.next().getType())) {
                    lastOutGood = true;
                }
            }
            if (!lastOutGood) {
                return null;
            }

            boolean nextInGood = false;
            Iterator<KrakEdge> nextIns = graph.outGoingEdges(last);
            while (nextIns.hasNext() && !nextInGood) {
                if (request.canTravel(nextIns.next().getType())) {
                    nextInGood = true;
                }
            }
            if (!nextInGood) {
                return null;
            }
        }

        last = null;
        next = places.get(0);

        for (int i = 0; i < places.size(); i++) {
            KrakNode place = places.get(i);
            last = next;
            next = place;
            boolean route = false;

            //Check for buffered route
            for (Route r : bufferedRoutes) {
                if (request.hasTravellerProfile(r.getTravellerProfile())) {
                    if (r.getStartNode() == last && r.getEndNode() == next) {
                        result.add(r);
                        route = true;
                        break;
                    }
                }
            }

            //Find route in graph
            if (!route) {
                List<KrakEdge> res = graph.easiestRoute(last, next, new Heuristics<KrakNode>() {

                    @Override
                    public double h(KrakNode start, KrakNode node) {
                        return start.getMapPoint().dist(node.getMapPoint()) / request.getMaxSpeed();
                    }
                });
                if (res == null) {
                    return null;
                }
                Route tempRoute = new Route(res, last, next, request.getTravellerProfile());
                addBufferedRoute(tempRoute); //Add the found route to buffering..
                result.add(tempRoute);
            }
        }
        return new RoutePlan(result);
    }

    private void addBufferedRoute(Route r) {
        bufferedRoutes.add(r);
        if (bufferedRoutes.size() > 1000) {
            bufferedRoutes.removeFirst();
        }
    }
}
